// https://www.npmjs.com/package/string-format
/**
 * 1) format('{{}},{}, you have {} unread message{}', 'Steve', 1)
 * => {},Steve, you have 1 unread message
 * 2) format('{0}, you have {1} unread message{2}{3}{4}', 'Holly', 2)
 * => Holly, you have 2 unread message
 * 3) format('The name\'s {1}. {0} {1}.', 'James', 'Bond')
 * => The name's Bond. James Bond.
 * 4) format('{0.firstName} {0.lastName} vs. {1.firstName} {1.lastName}',
 *   { firstName: 'Bobby', lastName: 'Fischer' },
 *   { firstName: 'Garry', lastName: 'Kasparov' })
 * => Bobby Fischer vs. Garry Kasparov
 * 5) format('{firstName}.{lastName} or {fullName} or {fullName.toUpperCase} was born at {birth.toISOString}',
 *   {
 *     firstName: 'Sheldon',
 *     lastName: 'Cooper',
 *     birth: new Date('2022-10-01'),
 *     fullName: function() { return this.firstName + ' ' + this.lastName }
 *   })
 * => Sheldon.Cooper Sheldon Cooper or SHELDON COOPER was born at 2022-10-01T00:00:00.000Z
 * 4) format('{0|upper}!, <a href="{1.url | escape}">{1.name | escape}</a>',
 *    'Apple',
 *    {
 *      name: 'Anchor & Hope',
 *      url: 'http://anchorandhopesf.com/',
 *    })
 * => The name's Bond. James Bond.
 * @param template
 * @returns {string}
 */
const format_transformers: Record<string, Function> = {
  upper: (s: string) => s.toUpperCase(),
  escape: (s: string) => s.replace(/[&<>"'`]/g, c => '&#' + c.charCodeAt(0) + ';'),
}

export function strFormat(template: string, ...args: any[]): string {
  let idx = 0
  let state = 'UNDEFINED'

  return template.replace(
    /([{}])\1|[{](.*?)(?:\s*\|\s*(.+?))?[}]/g,
    function (match, literal, _key, xf) {
      if (literal != null) {
        return literal
      }
      let key = _key
      if (key.length > 0) {
        if (state === 'IMPLICIT') {
          throw Error('cannot switch from implicit to explicit numbering')
        }
        state = 'EXPLICIT'
      } else {
        if (state === 'EXPLICIT') {
          throw Error('cannot switch from explicit to implicit numbering')
        }
        state = 'IMPLICIT'
        key = String(idx)
        idx += 1
      }

      //  1.  Split the key into a lookup path.
      //  2.  If the first path component is not an index, prepend '0'.
      //  3.  Reduce the lookup path to a single result. If the lookup
      //      succeeds the result is a singleton array containing the
      //      value at the lookup path; otherwise the result is [].
      //  4.  Unwrap the result by reducing with '' as the default value.
      const path = key.split('.')
      const value = (/^\d+$/.test(path[0]) ? path : ['0'].concat(path))
        .reduce(function (maybe: string[][], key: string) {
          return maybe.reduce(function (_: string[], x: any) {
            return x != null && key in Object(x) ?
              [typeof x[key] === 'function' ? x[key]() : x[key]] :
              []
          }, [])
        }, [args])
        .reduce((_: string[], x: string[]) => x, '')

      if (xf == null) {
        return value
      } else if (Object.prototype.hasOwnProperty.call(format_transformers, xf)) {
        return format_transformers[xf](value)
      } else {
        throw Error('no transformer named "' + xf + '"')
      }
    }
  )
}
